logo头像

野渡's小小知识乐园

第9章 虚拟内存之Linux内存系统

本节按照书籍实例,用一个实际系统的案例研究来总结虚拟内存的讨论,这是一个运行于Linux的Inter Core i7。需要注意的是,虽然我们说64位系统,而且处理器体系也允许64位的虚拟地址空间,但是实际上,Core i7现在只是支持48位(256TB)虚拟地址空间和52位(4PB)物理地址空间,兼容支持32位(4GB)地址空间

1、Core i7 内存系统结构

Core i7的内存系统的结构如下:

由图可知:

  • 处理器包括4个核、所有核共享L3高速缓存和DDR3内存控制器。
  • 每个核包含一个层次结构的TLB、一个层次结构的指令高速缓存,以及一组快速的点到点链路,这种QuickPath技术用于与其他核和外部I/O桥直接通信。
  • TLB是虚拟寻址的,四路组相联。L1、L2和L3都是物理寻址,分别位8、8和16路组相联。
  • Linux使用的是4KB的页。

2、Core i7 地址翻译过程

翻译过程如图9-22:

由图可知Core i7的地址翻译采用了TLB、高速缓存、多级页表等机制,需要注意的是:

  • Core i7采用四级页表层次结构。同时每个进程都有它自己的页表层次结构。
  • 进程运行时,虽然允许页表换进换出,但是与分配了的页相关联的页表都是驻留在内存中的。
  • CR3控制器指向第一级页表的起始地址,CR3的值是每个进程上下文的一部分,每次上下文切换时,CR3的值都会被恢复

3、第四级页表条目

图9-24给出了第四级页表中条目的格式:

注意一下几点:

  • PTE(page table entry)有三个权限位,控制对页的访问,分别是R/W控制读写,U/S是否能在用户模式中访问,XD(禁止执行),禁止从某些内存页取指令,防止缓冲区溢出攻击。
  • MMU翻译虚拟地址时,还会更新另外两个内核缺页处理程序会用到的位。A位称为引用位,内核用这个引用位来实现它的页替换算法。D位(修改位/脏位),告诉内核在复制替换页之前是否必须写回牺牲页

图9-25给出Core i7如何使用四级页表来将虚拟地址翻译成物理地址的。36位VPN被划分为四个9位的片,每个片被用作到一个页表的偏移量,CR3寄存器(控制寄存器3)包含L1页表的物理地址。VPN1提供到一个L1 PTE的偏移量,这个PTE包含L2页表的基地址。VPN2提供一个到L2 PTE的偏移量,以此类推。

优化地址翻译
当CPU需要翻译个虚拟地址时,它就发送一VPN到MMU,发送VPO到高速L1缓存。当MMU向TLB请求一个页表条目时,L1高速缓存正忙着利用VPO位查找对应的组,并读取这个组里的8个标记和相应的数据字。然后等MMU拿到PPN后直接就可以和8个标记进行匹配,决定是否取出其中的值。

4、Linux虚拟内存系统

本节主要是了解一个实际的操作系统如何组织虚拟内存和处理缺页。

Linux为每个进程维护了一个单独的虚拟地址空间如图9-26:

进程地址空间分为内核虚拟内存和进程虚拟内存

  • 进程虚拟内存包括进程的代码和数据段、堆和共享库以及栈段。
  • 内核虚拟内存包含内核中的代码和数据结构。内核虚拟内存的某些区域被映射到所有进程共享的物理页面。例如,每个进程共享内核的代码和全局数据结构,有趣的是,linux也将一组连续的虚拟页面(大小等于DRAM)映射到相应的一组连续的物理页面,为内核提供了便利的方法来访问物理内存中的任何特定位置
  • 内核虚拟内存的其他区域包含每个进程都不相同的数据,比如说页表和内核在进程的上下文中执行代码时使用的栈,以及记录虚拟地址空间当前组织的各种数据结构。

4.1 linux虚拟内存区域

Linux将虚拟内存组织成一些区域(也叫做段)的集合。一个区域(area)就是已经存在着的(已分配的)虚拟内存的连续片。例如,代码段,数据段,堆,共享库段,以及用户栈都是不同的区域。

每个存在虚拟页面都保存在某个区域中,而不属于某个区域的虚拟页是不存在,并且不能被进程引用。区域的概念很重要,因为它允许虚拟地址空间有间隙。内核不用记录那些不存在的虚拟页,而这样的页也不占用内存、磁盘或者内存本身中的任何额外资源。

如上图,内核为系统中的每个进程维护一个单独的任务结构(源代码中的task_struct)。任务结构中的元素包含或者指向内核运行该进程所需要的所有信息(例如,PID,指向用户栈的指针,可执行目标文件的名字,以及程序计数器)。

任务结构中的一个条目指向mm_struct,它描述了虚拟内存的当前状态。我们感兴趣的两个字段是pgd和mmap,其中pgd指向第一级页表(页全局目录)的基址,而mmap指向一个vm_area_structs(区域结构)的链表,其中每个vm_area_structs都描述了当前虚拟地址空间的一个区域。当内核运行这个进程时,就将pgd存放在CR3控制寄存器中。

4.2 linux缺页异常处理

假设MMU在试图翻译莫格虚拟地址A时,触发了一个缺页,这个异常导致控制转移到内核的缺页处理程序,进行如下处理:

  • (1)判断虚拟地址A是合法的吗?是在某个区域结构定义的区域内吗?缺页处理程序搜索区域结构链表(在链表中构建树来查找),如果不合法,触发一个段错误,终止进程。对应图中情况1
  • (2)试图进行的内存访问是否合法?权限对吗?对应图中情况2
  • (3)如果是对合法虚拟地址的合法操作,那么就选择一个牺牲页面,如果这个牺牲页面被修改过,就将它交换出去,换入新的页面并更新页表。

5、内存映射

Linux通过将一个虚拟内存区域与一个磁盘上的对象关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射。虚拟内存区域可以映射到两种类型的对象中的一种:

  • Linux文件系统中的普通文件:一个区域可以映射到一个普通磁盘文件的连续部分,例如一个可执行的目标文件。文件区被分成页大小的片,每一片包含一个虚拟页面的初始内容。因为按需进行页面调度,所以这些虚拟页面没有实际交换进入物理内存,直到CPU第一次引用到页面。如果区域比文件区要大,那么就用零来填充这个区域的余下部分。

  • 匿名文件:一个区域也可以映射到一个匿名文件,匿名文件是由内核创建的,包含的全是二进制零。CPU第一次引用这样一个区域内的虚拟页面时,内核就在物理内存中找到一个合适的牺牲页面,如果该页面被修改过,就将这个页面换出来,用二进制零覆盖牺牲页面并更新页面表,将该页面标记为是驻留在内存中的。注意在磁盘和内存之间并没有实际的数据传送。因为这个原因,映射到匿名文件的区域中的页面有时也叫做请求二进制零的页。

无论哪种情况下,一旦一个虚拟页面被初始化了,它就在一个由内核维护的专门的交换文件之间换来换去。交换文件也叫作交换空间或者交换区域。需要意识到的很重要的一点是,在任何时刻,交换空间都限制着当前运行着的进程能够分配的虚拟页面的总数。

6、再看共享对象

通过内存映射,一个对象可以被映射到虚拟内存的一个区域,要么作为共享对象,要么作为私用对象。

6.1 共享对象

一个进程将共享对象映射到它的虚拟空间的一个区域内,那么这个进程对这个区域的所有写操作,对于那些也把这个共享对象映射到它们虚拟空间内的进程来说,都是可见的。这些变化也都会反映在磁盘的原始对象中,多个进程通过将内存映射和将页表条目指向相同的物理页面实现贡献对象,此时物理内存中只需要存放共享对象的一个副本

6.2 私有对象写时复制(COW)

私有对象采用的是写时复制(copy on write),一个私有对象开始生命周期的方式基本和共享对象一样,在物理内存上只保留一份副本。

对于每个映射私有对象的进程,相应私有区域的页表条目都被标记为只读,并且区域结构被标记为私有的写时复制。

一个进程试图写私有区域内的某个页面,那么这个写操作就会触发一个保护故障,故障处理程序会在物理内存中创建这个页面的一个新副本,更新页表条目指向这个新副本,然后恢复这个页面的可写权限,如下图所示。之后重新执行这个写指令,则写操作可以正常执行。

通过延迟私有对象中的拷贝直到最后可能的时刻,写时拷贝最充分的使用了稀有的物理存储器

7、再看fork函数

fork函数是如何创建一个带有自己独立虚拟地址空间的新进程的?

当fork函数被当前进程调用时,内核为新进程创建了各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟存储器,它创建了当前进程的mm_struct、区域结构和页表的原样拷贝。它将两个进程中的每个页表都标记为只读,并将两个进程中的区域结构都标记为私有(设置vm_flags)的写时拷贝。

当fork在新进程中返回时,新进程现在的虚拟存储器刚好和调用fork时存在的虚拟存储器相同。当这两个进程中任一个后来进行写操作时,写时拷贝机制就会创建新页表,因此,也就为每个进程保持了私有地址空间的抽象概念。

8、再看execve函数

假设运行在当前进程中的程序执行了如下的调用:

1
execve("a.out", NULL, NULL);

execve函数在当前进程中加载并运行包含在可执行目标文件a.out中的程序,用a.out程序有效替代了当前程序。加载并运行a.out需要以下几个步骤:

  • 删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
  • 映射私有区域。为新程序的文本、数据、bss(映射到匿名文件)和栈区域创建新的区域结构。所有这些新的区域都是私有的,写时拷贝的。
  • 映射共享区域。如果a.out程序与共享对象或目标链接,比如标准库,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域中。
  • 设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向文本区域的入口点。

9、使用mmap函数的用户级内存映射

mmap函数为我们在进程的虚拟空间开辟一块新的虚拟内存,可以将一个对象(如文件)映射到这块新的虚拟内存,所以操作新的虚拟内存就是操作这个文件,下面我将介绍mmap函数的运用。

1
2
#include <sys/mman.h>
void *mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset);

各个参数的意义如下:

  • start表示新的虚拟内存从这个地址开始,一般来说取NULL,那么将有内核来分配。
  • length表示新的虚拟内存的大小。
  • prot表示这块新的虚拟内存的访问权限:
    PROT_EXEC:可执行
    PROT_READ:可读
    PROT_WRITE:可写
    PROT_NONE:无法访问
  • flags标识被映射对象匿名对象、私有对象或共享对象。

eg:

1
bufp=Mmap(NULL,size,PROT_READ,MAP_PRIVATE|MAP_ANON,0,0);

表示让内核创建一个新的包含size字节的只读、私有、请求二进制0的虚拟内存区域。如果调用成功,那么bufp包含新区域的地址。

munmap用于删除虚拟内存的区域。